package com.ctrip.framework.apollo.portal.spi.configuration;

import com.ctrip.framework.apollo.common.condition.ConditionalOnMissingProfile;
import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
import com.ctrip.framework.apollo.portal.spi.LogoutHandler;
import com.ctrip.framework.apollo.portal.spi.SsoHeartbeatHandler;
import com.ctrip.framework.apollo.portal.spi.UserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.UserService;
import com.ctrip.framework.apollo.portal.spi.ctrip.CtripLogoutHandler;
import com.ctrip.framework.apollo.portal.spi.ctrip.CtripSsoHeartbeatHandler;
import com.ctrip.framework.apollo.portal.spi.ctrip.CtripUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.ctrip.CtripUserService;
import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultLogoutHandler;
import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultSsoHeartbeatHandler;
import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.defaultimpl.DefaultUserService;
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserInfoHolder;
import com.ctrip.framework.apollo.portal.spi.springsecurity.SpringSecurityUserService;
import com.google.common.collect.Maps;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.embedded.FilterRegistrationBean;
import org.springframework.boot.context.embedded.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;

import javax.servlet.Filter;
import java.util.EventListener;
import java.util.Map;

/**
 * 认证 Spring 配置
 */
@Configuration
public class AuthConfiguration {

    // 实现方式一：携程内部实现，接入了SSO并实现用户搜索、查询接口

    /**
     * spring.profiles.active = ctrip
     */
    @Configuration
    @Profile("ctrip")
    static class CtripAuthAutoConfiguration {

        @Autowired
        private PortalConfig portalConfig;

        @Bean
        public ServletListenerRegistrationBean redisAppSettingListner() {
            ServletListenerRegistrationBean redisAppSettingListener = new ServletListenerRegistrationBean();
            redisAppSettingListener.setListener(listener("org.jasig.cas.client.credis.CRedisAppSettingListner"));
            return redisAppSettingListener;
        }

        @Bean
        public ServletListenerRegistrationBean singleSignOutHttpSessionListener() { // 登出 EventListener
            ServletListenerRegistrationBean singleSignOutHttpSessionListener = new ServletListenerRegistrationBean();
            singleSignOutHttpSessionListener.setListener(listener("org.jasig.cas.client.session.SingleSignOutHttpSessionListener"));
            return singleSignOutHttpSessionListener;
        }

        @Bean
        public FilterRegistrationBean casFilter() { // 登出 Filter
            FilterRegistrationBean singleSignOutFilter = new FilterRegistrationBean();
            singleSignOutFilter.setFilter(filter("org.jasig.cas.client.session.SingleSignOutFilter"));
            singleSignOutFilter.addUrlPatterns("/*");
            singleSignOutFilter.setOrder(1);
            return singleSignOutFilter;
        }

        @Bean
        public FilterRegistrationBean authenticationFilter() {
            FilterRegistrationBean casFilter = new FilterRegistrationBean();

            Map<String, String> filterInitParam = Maps.newHashMap();
            filterInitParam.put("redisClusterName", "casClientPrincipal");
            filterInitParam.put("serverName", portalConfig.portalServerName());
            filterInitParam.put("casServerLoginUrl", portalConfig.casServerLoginUrl());
            //we don't want to use session to store login information, since we will be deployed to a cluster, not a single instance
            filterInitParam.put("useSession", "false");
            filterInitParam.put("/openapi.*", "exclude");

            casFilter.setInitParameters(filterInitParam);
            casFilter.setFilter(filter("com.ctrip.framework.apollo.sso.filter.ApolloAuthenticationFilter"));
            casFilter.addUrlPatterns("/*");
            casFilter.setOrder(2);

            return casFilter;
        }

        @Bean
        public FilterRegistrationBean casValidationFilter() {
            FilterRegistrationBean casValidationFilter = new FilterRegistrationBean();
            Map<String, String> filterInitParam = Maps.newHashMap();
            filterInitParam.put("casServerUrlPrefix", portalConfig.casServerUrlPrefix());
            filterInitParam.put("serverName", portalConfig.portalServerName());
            filterInitParam.put("encoding", "UTF-8");
            //we don't want to use session to store login information, since we will be deployed to a cluster, not a single instance
            filterInitParam.put("useSession", "false");
            filterInitParam.put("useRedis", "true");
            filterInitParam.put("redisClusterName", "casClientPrincipal");

            casValidationFilter.setFilter(filter("org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter"));
            casValidationFilter.setInitParameters(filterInitParam);
            casValidationFilter.addUrlPatterns("/*");
            casValidationFilter.setOrder(3);

            return casValidationFilter;

        }

        @Bean
        public FilterRegistrationBean assertionHolder() {
            FilterRegistrationBean assertionHolderFilter = new FilterRegistrationBean();

            Map<String, String> filterInitParam = Maps.newHashMap();
            filterInitParam.put("/openapi.*", "exclude");

            assertionHolderFilter.setInitParameters(filterInitParam);

            assertionHolderFilter.setFilter(filter("com.ctrip.framework.apollo.sso.filter.ApolloAssertionThreadLocalFilter"));
            assertionHolderFilter.addUrlPatterns("/*");
            assertionHolderFilter.setOrder(4);

            return assertionHolderFilter;
        }

        @Bean
        public CtripUserInfoHolder ctripUserInfoHolder() {
            return new CtripUserInfoHolder();
        }

        @Bean
        public CtripLogoutHandler logoutHandler() {
            return new CtripLogoutHandler();
        }

        private Filter filter(String className) {
            Class clazz = null;
            try {
                clazz = Class.forName(className);
                Object obj = clazz.newInstance();
                return (Filter) obj;
            } catch (Exception e) {
                throw new RuntimeException("instance filter fail", e);
            }

        }

        private EventListener listener(String className) {
            Class clazz;
            try {
                clazz = Class.forName(className);
                Object obj = clazz.newInstance();
                return (EventListener) obj;
            } catch (Exception e) {
                throw new RuntimeException("instance listener fail", e);
            }
        }

        @Bean
        public UserService ctripUserService(PortalConfig portalConfig) {
            return new CtripUserService(portalConfig);
        }

        @Bean
        public SsoHeartbeatHandler ctripSsoHeartbeatHandler() {
            return new CtripSsoHeartbeatHandler();
        }

    }

    // 实现方式二：使用 Apollo 提供的 Spring Security 简单认证

    /**
     * spring.profiles.active = auth
     */
    @Configuration
    @Profile("auth")
    static class SpringSecurityAuthAutoConfiguration {

        @Bean
        @ConditionalOnMissingBean(SsoHeartbeatHandler.class)
        public SsoHeartbeatHandler defaultSsoHeartbeatHandler() {
            return new DefaultSsoHeartbeatHandler();
        }

        @Bean
        @ConditionalOnMissingBean(UserInfoHolder.class)
        public UserInfoHolder springSecurityUserInfoHolder() {
            return new SpringSecurityUserInfoHolder();
        }

        @Bean
        @ConditionalOnMissingBean(LogoutHandler.class)
        public LogoutHandler logoutHandler() {
            return new DefaultLogoutHandler();
        }

        // JdbcUserDetailsManager 扩展了 JdbcDaoImpl 的功能，提供了一些很有用的与 User 相关的方法
        @Bean
        public JdbcUserDetailsManager jdbcUserDetailsManager(AuthenticationManagerBuilder auth, DataSource datasource) throws Exception {
            JdbcUserDetailsManager jdbcUserDetailsManager = auth.jdbcAuthentication() // 基于 JDBC
                    .passwordEncoder(new BCryptPasswordEncoder()) // 加密方式为 BCryptPasswordEncoder
                    .dataSource(datasource) // 数据源
                    .usersByUsernameQuery("select Username,Password,Enabled from `Users` where Username = ?") // 使用 Username 查询 User
                    .authoritiesByUsernameQuery("select Username,Authority from `Authorities` where Username = ?") // 使用 Username 查询 Authorities
                    .getUserDetailsService();

            jdbcUserDetailsManager.setUserExistsSql("select Username from `Users` where Username = ?"); // 判断 User 是否存在
            jdbcUserDetailsManager.setCreateUserSql("insert into `Users` (Username, Password, Enabled) values (?,?,?)"); // 插入 User
            jdbcUserDetailsManager.setUpdateUserSql("update `Users` set Password = ?, Enabled = ? where Username = ?"); // 更新 User
            jdbcUserDetailsManager.setDeleteUserSql("delete from `Users` where Username = ?"); // 删除 User
            jdbcUserDetailsManager.setCreateAuthoritySql("insert into `Authorities` (Username, Authority) values (?,?)"); // 插入 Authorities
            jdbcUserDetailsManager.setDeleteUserAuthoritiesSql("delete from `Authorities` where Username = ?"); // 删除 Authorities
            jdbcUserDetailsManager.setChangePasswordSql("update `Users` set Password = ? where Username = ?"); // 更新 Authorities

            return jdbcUserDetailsManager;
        }

        @Bean
        @ConditionalOnMissingBean(UserService.class)
        public UserService springSecurityUserService() {
            return new SpringSecurityUserService();
        }

    }

    @Order(99)
    @Profile("auth")
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    static class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter {

        public static final String USER_ROLE = "user";

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable(); // 关闭打开的 csrf 保护
            http.headers().frameOptions().sameOrigin(); // 仅允许相同 origin 访问
            http.authorizeRequests()
                    .antMatchers("/openapi/**", "/vendor/**", "/styles/**", "/scripts/**", "/views/**", "/img/**").permitAll() // openapi 和 资源不校验权限
                    .antMatchers("/**").hasAnyRole(USER_ROLE); // 其他，需要登录 User
            http.formLogin().loginPage("/signin").permitAll().failureUrl("/signin?#/error").and().httpBasic(); // 登录页
            http.logout().invalidateHttpSession(true).clearAuthentication(true).logoutSuccessUrl("/signin?#/logout"); // 登出（退出）
            http.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/signin")); // 未身份校验，跳转到登录页
        }

    }

    // 实现方式三： 默认实现，全局只有 apollo 一个账号

    /**
     * default profile
     */
    @Configuration
    @ConditionalOnMissingProfile({"ctrip", "auth"})
    static class DefaultAuthAutoConfiguration {

        @Bean
        @ConditionalOnMissingBean(SsoHeartbeatHandler.class)
        public SsoHeartbeatHandler defaultSsoHeartbeatHandler() {
            return new DefaultSsoHeartbeatHandler();
        }

        @Bean
        @ConditionalOnMissingBean(UserInfoHolder.class)
        public DefaultUserInfoHolder defaultUserInfoHolder() {
            return new DefaultUserInfoHolder();
        }

        @Bean
        @ConditionalOnMissingBean(LogoutHandler.class)
        public DefaultLogoutHandler logoutHandler() {
            return new DefaultLogoutHandler();
        }

        @Bean
        @ConditionalOnMissingBean(UserService.class)
        public UserService defaultUserService() {
            return new DefaultUserService();
        }
    }

    @Configuration
    @ConditionalOnMissingProfile("auth") // 被【实现方式一】【实现方式三】共用
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    static class DefaultWebSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.csrf().disable();
            http.headers().frameOptions().sameOrigin();
        }
    }

}